#import <QuartzCore/QuartzCore.h>
#import "ALTimelineCanvas.h"
#import "ALCanvasPage.h"
#import "ALRubberBandView.h"
#import "ALLayoutSegment.h"
#import "ALScrollFixedElementAnimation.h"
#import "NSDate+SMRAdditions.h"


#pragma mark Constants
const CGFloat ALTimelineCanvasEndPadding = 10.0f;
const CGFloat ALTimelineCanvasTopPadding = 20.0f;
const CGFloat ALTimelineCanvasBottomPadding = 5.0f;
const CGFloat ALTimelineCanvasMinimumDragDistance = 3.0f;

#pragma mark Keys
static NSString* const ALTimelineAnimatedElement = @"ALTimelineAnimatedElement";
static NSString* const ALCanvasFrameOriginKey = @"frameOrigin";
static NSString* const ALCanvasMinimumViewSizeKey = @"minimumViewSize";


#pragma mark -
#pragma mark Class Globals

static NSGradient* ALTimelineCanvasBackgroundGradient;
static CABasicAnimation* ALTimelineFrameAnimation;


#pragma mark -
@interface ALTimelineCanvas ()
@property (nonatomic, assign) CGFloat pointsPerDay;
@property (nonatomic, retain) NSArray *pages;
@property (nonatomic, assign) ALResizeDirection resizeDirection;
@property (nonatomic, retain) NSArray *addedElements;
@property (nonatomic, retain) NSArray *removedElements;
@property (nonatomic, assign) NSSize minimumViewSize;
@property (nonatomic, assign) CGFloat triggerDenserHeight;
@property (nonatomic, assign) CGFloat triggerSparserHeight;
@property (nonatomic, assign) BOOL fitWidth;
@property (nonatomic, retain) ALTimelineElement *fixedLayoutElement;
@end


#pragma mark -
@interface ALTimelineCanvas (ALTimelineCanvasPrivateMethods)
-(id<ALTimelineViewDelegate>)delegate;
-(id<ALTimelineViewDragDropDelegate>)dragDropDelegate;
-(NSArray *)visiblePages_;
-(NSRect)bottomTrackingRectForElementRect_:(NSRect)elementRect;
-(NSRect)topTrackingRectForElementRect_:(NSRect)elementRect;
-(NSRect)rightTrackingRectForElementRect_:(NSRect)elementRect;
-(void)addTrackingAreaForElement_:(ALTimelineElement *)element withDirection:(ALResizeDirection)direction;
-(void)addTrackingAreasForElement_:(ALTimelineElement *)element;
-(void)autoScrollTimerDidFire_:(NSTimer *)timer;
-(void)initiateDragWithEvent_:(NSEvent *)theEvent forElement:(ALTimelineElement *)element;
-(NSRect)rectFromPoint_:(NSPoint)firstPoint toPoint:(NSPoint)secondPoint;
-(NSArray *)visibleElements_;
-(CGFloat)minimumPointsPerDay_;
-(BOOL)determineLayoutPositions_;
-(void)displayAddedElements_;
-(void)displayRemovedElements_;
-(void)displayVisibleElements_:(NSArray *)elements;
-(NSSize)preferredSizeForElement_:(ALTimelineElement *)anElement;
-(NSSize)persistedSizeForElement_:(ALTimelineElement *)element;
-(void)mergeSegments_:(NSMutableArray *)segments inRange:(NSRange)mergeRange withXPosition:(CGFloat)xPosition;
-(BOOL)areElementsLeftAlignedWithElementIndex_:(NSUInteger)elementIndex;
-(BOOL)adjustPointsPerDayWithPositions_:(NSArray *)positions;
-(NSArray *)timelineElements;
-(void)updateVisibleElements_;
-(void)handleClickWithEvent_:(NSEvent *)mouseUpEvent;
-(void)updateElementsWithPositions:(NSArray *)positions;
@end


#pragma mark -
@implementation ALTimelineCanvas

@synthesize timeline;
@synthesize needsLayout;
@synthesize pointsPerDay;
@synthesize pages;
@synthesize resizeDirection;
@synthesize addedElements;
@synthesize removedElements;
@synthesize minimumViewSize;
@synthesize triggerDenserHeight;
@synthesize triggerSparserHeight;
@synthesize fitWidth;
@synthesize fixedLayoutElement;
@synthesize endPadding;
@synthesize topPadding;
@synthesize bottomPadding;


#pragma mark -
#pragma mark Initialization

+(void)initialize {
	ALTimelineCanvasBackgroundGradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:0.3f alpha:1.0f] endingColor:[NSColor colorWithCalibratedWhite:0.2f alpha:1.0f]];
	
	ALTimelineFrameAnimation = [[CABasicAnimation animation] retain];
	[ALTimelineFrameAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
}

-(id)initWithFrame:(NSRect)frameRect {
	if (!(self = [super initWithFrame:frameRect]))
		return nil;
	[self setEndPadding:ALTimelineCanvasEndPadding];
	[self setTopPadding:ALTimelineCanvasTopPadding];
	[self setBottomPadding:ALTimelineCanvasBottomPadding];
	return self;
}

-(void)viewDidMoveToSuperview {
	NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
	if ([self superview]) 
		[noteCenter addObserver:self selector:@selector(canvasDidChangeFrame:) name:NSViewFrameDidChangeNotification object:self];
	else 
		[noteCenter removeObserver:self name:NSViewFrameDidChangeNotification object:nil];
}

-(void)viewDidMoveToWindow {
	NSScrollView *enclosingScrollView = [self enclosingScrollView];
	if (!enclosingScrollView)
		return;
	NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
	if ([self window])
		[noteCenter addObserver:self selector:@selector(viewDidScroll:) name:NSViewBoundsDidChangeNotification object:[enclosingScrollView contentView]];
	else 
		[noteCenter removeObserver:self name:NSViewBoundsDidChangeNotification object:nil];
}

-(void)initializeAddedElements_ {
	for (ALTimelineElement *element in [self addedElements]) {
		CAAnimation *frameAnimation = [ALTimelineFrameAnimation copy];
		[frameAnimation setDelegate:self];
		[frameAnimation setValue:element forKey:ALTimelineAnimatedElement];
		[[element view] setAnimations:[NSDictionary dictionaryWithObjectsAndKeys:frameAnimation, ALCanvasFrameOriginKey, nil]];
	}
}


#pragma mark -
#pragma mark Accessors

-(id<ALTimelineViewDragDropDelegate>)dragDropDelegate {
	return [[self timeline] dragDropDelegate];
}

-(id<ALTimelineViewDelegate>)delegate {
	return [[self timeline] delegate];
}

-(NSArray *)timelineElements {
	return [[self timeline] timelineElements];
}

+(NSSet *)keyPathsForValuesAffectingMinimumSize {
	return [NSSet setWithObject:ALCanvasMinimumViewSizeKey];
}

-(NSSize)minimumSize {
	return [self minimumViewSize];
}

-(void)setTimeline:(ALTimelineView *)aTimeline {
    NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
	[noteCenter removeObserver:self name:ALTimelineElementsDidChangeNotification object:nil];
	timeline = aTimeline;
	if (aTimeline)
		[noteCenter addObserver:self selector:@selector(timelineElementsDidChange:) name:ALTimelineElementsDidChangeNotification object:aTimeline];
}


#pragma mark -
#pragma mark Drawing

-(void)viewWillDraw {
	while ([self needsLayout])
		[self layout];
	[super viewWillDraw];
}

-(void)drawRect:(NSRect)dirtyRect {
	[ALTimelineCanvasBackgroundGradient drawInRect:[self bounds] angle:90];
}


#pragma mark -
#pragma mark Layout

-(void)setNeedsLayout:(BOOL)shouldLayout {
	needsLayout = shouldLayout;
	if (needsLayout)
		[self setNeedsDisplay:YES];
}

-(BOOL)isLayoutRequiredForSize:(NSSize)canvasSize {
	if ([[self timelineElements] count] == 0) 
		return NO;
	NSSize minViewSize = [self minimumSize];
	if ([self fitWidth] && minViewSize.width != NSWidth([self visibleRect]))
		return YES;
	if (minViewSize.width < canvasSize.width) 
		return YES;
	if (canvasSize.height < [self triggerSparserHeight] || canvasSize.height > [self triggerDenserHeight]) 
		return YES;
	return NO;
}

-(void)layout {
	[self setNeedsLayout:NO];
	NSArray *oldVisibleElements = [self visibleElements_];
	[self setPointsPerDay:[self minimumPointsPerDay_]];
	BOOL  layoutSuccessful = NO;
	while (!layoutSuccessful)
		layoutSuccessful = [self determineLayoutPositions_];
	NSArray *newVisibleElements = [self visibleElements_];
	NSMutableSet *visibleElementsSet = [NSMutableSet setWithArray:oldVisibleElements];
	[visibleElementsSet addObjectsFromArray:newVisibleElements];
	[self displayVisibleElements_:[visibleElementsSet allObjects]];
	[self displayAddedElements_];
	[self displayRemovedElements_];
}

-(void)displayAddedElements_ {
	NSArray *added = [self addedElements];
	if ([added count] == 0)
		return;
	[[self timeline] scrollElementToVisible:[added objectAtIndex:0]];
	for (ALTimelineElement *element in added) {
		if (![self isElementVisible:element])
			continue;
		[[element view] setFrame:[element currentPosition]];
		[self addSubview:[element view] positioned:NSWindowAbove relativeTo:nil];
	}
	[self setAddedElements:nil];
}

-(void)displayRemovedElements_ {
	NSArray *removed = [self removedElements];
	if ([removed count] == 0)
		return;
	for (ALTimelineElement *element in removed) {
		[[element view] removeFromSuperview];
	}
	[self setRemovedElements:nil];
}

-(void)displayVisibleElements_:(NSArray *)elements {
	if ([elements count] == 0) 
		return;
	for (ALTimelineElement *element in elements) {
		if ([[self addedElements] containsObject:element] || [[self removedElements] containsObject:element]) 
			continue;
		NSView *elementView = [element view];
		if (![elementView superview])
			[self addSubview:elementView];
		NSRect newPosition = [element currentPosition];
		[[elementView animator] setFrame:newPosition];
	}
}

-(CGFloat)minimumPointsPerDay_ {
	NSUInteger elementCount = [[self timelineElements] count];
	if (elementCount == 0)
		return 0;
	NSSize visibleSize = [self visibleRect].size;
//	if (elementCount == 1)
//		return visibleSize.width / 2;
	ALTimelineElement *lastElement = [[self timelineElements] lastObject];
	CGFloat lastElementWidth = [self preferredSizeForElement_:lastElement].width;	
	CGFloat points = visibleSize.width - (2 * ALTimelineCanvasEndPadding) - lastElementWidth;
	NSInteger days = [lastElement daysFromFirst];
	if (days == 0)
		return visibleSize.width / 2;
	return points/days;
}

-(BOOL)determineLayoutPositions_ {
	NSMutableArray *segments = [NSMutableArray array];
	NSMutableArray *newPages = [NSMutableArray array];
	NSMutableArray *elementRects = [NSMutableArray arrayWithCapacity:[[self timelineElements] count]];
	ALCanvasPage *currentPage = [[ALCanvasPage alloc] initWithLeftBoundary:0];
	[newPages addObject:currentPage];
	CGFloat aggregateHeight = 0;
	NSSize minSize = NSZeroSize;
	for (NSUInteger elementIndex = 0; elementIndex < [[self timelineElements] count]; elementIndex++) {
		ALTimelineElement *element = [[self timelineElements] objectAtIndex:elementIndex];
		NSUInteger elementDays = [element daysFromFirst];
		CGFloat elementx = floor([self pointsPerDay] * elementDays) + ALTimelineCanvasEndPadding;
		NSSize elementSize = [self preferredSizeForElement_:element];
		aggregateHeight += elementSize.height;
		CGFloat totalHeight = 0;
		NSRange affectedSegments = NSMakeRange(0, 0);
		
		// Scan the layout segments to find a suitable position for the current element
		for (NSUInteger segmentIndex = 0; segmentIndex < [segments count]; segmentIndex++) {
			if (affectedSegments.length == 0)
				affectedSegments.location = segmentIndex;
			ALLayoutSegment *currentSegment = [segments objectAtIndex:segmentIndex];
			if (elementx < [currentSegment x]) {
				totalHeight = 0;
				affectedSegments = NSMakeRange(segmentIndex, 0);
				continue;
			}
			totalHeight += [currentSegment height];
			affectedSegments.length += 1;
			if (totalHeight >= elementSize.height)
				break;
		}
		CGFloat segmentx = elementx + elementSize.width;
		ALLayoutSegment *locationSegment = [segments count] == 0 ? nil : [segments objectAtIndex:affectedSegments.location];
		CGFloat locationMaxY = locationSegment == nil ? ALTimelineCanvasBottomPadding : [locationSegment maxY];
		CGFloat segmenty = totalHeight == 0 ? locationMaxY : [locationSegment y];
		
		// Adjust the segments to incorporate the new element
		ALLayoutSegment *insertSegment = [[ALLayoutSegment alloc] initWithX:segmentx withY:segmenty withHeight:elementSize.height];
		if (totalHeight == 0) 
			[segments addObject:insertSegment];
		else if (totalHeight > elementSize.height) {
			ALLayoutSegment *topSegment = [segments objectAtIndex:affectedSegments.location + affectedSegments.length - 1];
			[self mergeSegments_:segments inRange:affectedSegments withXPosition:[topSegment x]];
			ALLayoutSegment *mergedSegment = [segments objectAtIndex:affectedSegments.location];
			[mergedSegment setHeight:totalHeight - elementSize.height];
			[mergedSegment setY:segmenty + elementSize.height];
			[segments insertObject:insertSegment atIndex:affectedSegments.location];
		}
		else {
			[segments removeObjectsInRange:affectedSegments];
			[segments insertObject:insertSegment atIndex:affectedSegments.location];
		}
		
		NSRect elementRect = NSMakeRect(elementx, segmenty, elementSize.width, elementSize.height);
		[elementRects addObject:[NSValue valueWithRect:elementRect]];
		// Check if height of element exceeds the visible height of the view.  If so, check whether there is a column of elements on the same day so that adjusting the points per day won't spread them apart and thus we'll have to accept a view height greater than the visible height (vertical scroll bar appears when present).  If at least one element isn't on the same day, then increase the points per day (sparser) the minimum amount to make the elements shift.  Return and rerun the method with the new points per day
		if (segmenty + elementSize.height > NSMaxY([self visibleRect]) - ALTimelineCanvasTopPadding && 
			![self areElementsLeftAlignedWithElementIndex_:elementIndex] && 
			[self adjustPointsPerDayWithPositions_:elementRects]) {
			return NO;
		}
		
		while (elementRect.origin.x > [currentPage rightBoundary]) {
			currentPage = [[ALCanvasPage alloc] initWithLeftBoundary:[currentPage rightBoundary] + 1];
			[newPages addObject:currentPage];
		}
		[currentPage addElement:element atRect:elementRect];
		
		minSize.height = MAX(NSMaxY(elementRect), minSize.height);
		minSize.width = MAX(NSMaxX(elementRect) + ALTimelineCanvasEndPadding, minSize.width);
	}
	[self updateElementsWithPositions:elementRects];
	[self setPages:newPages];
	CGFloat averageheight = aggregateHeight / [[self timelineElements] count];
	[self setTriggerDenserHeight:minSize.height + averageheight + ALTimelineCanvasTopPadding];
	[self setTriggerSparserHeight:minSize.height + ALTimelineCanvasTopPadding];
	[self setFitWidth:minSize.width == NSWidth([self visibleRect])];
	[self setMinimumViewSize:minSize];
	return YES;
}

-(void)updateElementsWithPositions:(NSArray *)positions {
	for (NSUInteger elementRectIndex = 0; elementRectIndex < [positions count]; elementRectIndex++) {
		NSRect elementRect = [[positions objectAtIndex:elementRectIndex] rectValue];
		ALTimelineElement *elementToUpdate = [[self timelineElements] objectAtIndex:elementRectIndex];
		[elementToUpdate setPreviousPosition:[elementToUpdate currentPosition]];
		[elementToUpdate setCurrentPosition:elementRect];
	}
}

-(BOOL)areElementsLeftAlignedWithElementIndex_:(NSUInteger)elementIndex {
	if (elementIndex == 0)
		return YES;
	NSRect useableRect = [self useableRectForBounds:[self bounds]];
	CGFloat maxSize = useableRect.size.height;
	ALTimelineElement *element = [[self timelineElements] objectAtIndex:elementIndex];
	NSUInteger elementDays = [element daysFromFirst];
	CGFloat totalHeight = [element preferredSize].height;
	if (totalHeight > maxSize)
		return YES;
	NSArray *compareElements = [[self timelineElements] subarrayWithRange:NSMakeRange(0, elementIndex)];
	for (ALTimelineElement *compareElement in [compareElements reverseObjectEnumerator]) {
		if ([compareElement daysFromFirst] != elementDays)
			return NO;
		totalHeight += [compareElement preferredSize].height;
		if (totalHeight > maxSize)
			return YES;
	}
	return NO;
}

-(BOOL)adjustPointsPerDayWithPositions_:(NSArray *)positions {
	NSMutableIndexSet *indexesToCompare = [NSMutableIndexSet indexSet];
	__block NSMutableIndexSet *indexesToRemove = [NSMutableIndexSet indexSet];
	__block CGFloat leastPointsPerDay = CGFLOAT_MAX;
	
	for (NSUInteger elementToAdjustIndex = 1; elementToAdjustIndex < [positions count]; elementToAdjustIndex++) {
		ALTimelineElement *elementToAdjust = [[self timelineElements] objectAtIndex:elementToAdjustIndex];
		NSInteger elementToAdjustDays = [elementToAdjust daysFromFirst];
		NSRect elementToAdjustRect = [[positions objectAtIndex:elementToAdjustIndex] rectValue];
		CGFloat elementToAdjustOriginX = elementToAdjustRect.origin.x;
		[indexesToCompare addIndex:elementToAdjustIndex - 1];
		
		[indexesToCompare enumerateIndexesUsingBlock: ^(NSUInteger elementToCompareIndex, BOOL *stop) {
			ALTimelineElement *elementToCompare = [[self timelineElements] objectAtIndex:elementToCompareIndex];
			NSRect elementToCompareRect = [[positions objectAtIndex:elementToCompareIndex] rectValue];
			CGFloat elementToCompareMaxX = NSMaxX(elementToCompareRect);
			if (elementToCompareMaxX < elementToAdjustOriginX) {
				[indexesToRemove addIndex:elementToCompareIndex];
				return;
			}
			CGFloat elementToComparePoints = elementToCompareRect.size.width + 1;
			NSInteger elementToCompareDays = [elementToCompare daysFromFirst];
			NSInteger deltaDays = elementToAdjustDays - elementToCompareDays;
			if (deltaDays == 0) 
				return;
			CGFloat currentPointsPerDay = elementToComparePoints / deltaDays;
			if (currentPointsPerDay <= [self pointsPerDay]) 
				return;
			leastPointsPerDay = MIN(leastPointsPerDay, currentPointsPerDay);
		}];
		[indexesToCompare removeIndexes:indexesToRemove];
		[indexesToRemove removeAllIndexes];
	}
	if (leastPointsPerDay == CGFLOAT_MAX)
		return NO;
	[self setPointsPerDay:leastPointsPerDay];
	return YES;
}

-(void)mergeSegments_:(NSMutableArray *)segments inRange:(NSRange)mergeRange withXPosition:(CGFloat)xPosition {
	if (mergeRange.length == 0)
		return;
	ALLayoutSegment *mergedSegment = [segments objectAtIndex:mergeRange.location];
	[mergedSegment setX:xPosition];
	if (mergeRange.length == 1) 
		return;
	NSArray *segmentsToMerge = [segments subarrayWithRange:mergeRange];
	CGFloat totalHeight = 0;
	for (ALLayoutSegment *segment in segmentsToMerge) 
		totalHeight += [segment height];
	[mergedSegment setHeight:totalHeight];
	mergeRange.location += 1;
	mergeRange.length -= 1;
	[segments removeObjectsInRange:mergeRange];
}

-(NSArray *)visiblePages_ {
	NSRect visibleRect = [self visibleRect];
	NSMutableArray *pageArray = [NSMutableArray array];
	for (ALCanvasPage *page in [self pages]) {
		if ([page leftBoundary] > NSMaxX(visibleRect))
			break;
		if ([page maxX] < visibleRect.origin.x)
			continue;
		[pageArray addObject:page];
	}
	return pageArray;
}

-(NSArray *)visibleElements_ {
	NSMutableArray *elementsArray = [NSMutableArray array];
	for (ALCanvasPage *page in [self visiblePages_]) 
		[elementsArray addObjectsFromArray:[page elements]];
	return elementsArray;
}

-(NSSize)preferredSizeForElement_:(ALTimelineElement *)anElement {
	NSSize persistedSize = [self persistedSizeForElement_:anElement];
	if (!NSEqualSizes(persistedSize, NSZeroSize))
		return persistedSize;
	if (![[self delegate] respondsToSelector:@selector(timeline:preferredSizeForElement:)]) 
		return [anElement preferredSize];
	NSSize preferredSize = [[self delegate] timeline:[self timeline] preferredSizeForElement:anElement];
	if (!NSEqualSizes(preferredSize, NSZeroSize))
		return preferredSize;
	return [anElement preferredSize];
}

-(NSSize)persistedSizeForElement_:(ALTimelineElement *)element {
	if (![[self delegate] respondsToSelector:@selector(timeline:uniqueStringForRepresentedObject:)])
		return NSZeroSize;
	NSString *objectKey = [[self delegate] timeline:[self timeline] uniqueStringForRepresentedObject:[element representedObject]];
	if (!objectKey)
		return NSZeroSize;
	NSString *persistedSizeString = [[NSUserDefaults standardUserDefaults] stringForKey:objectKey];
	if (!persistedSizeString)
		return NSZeroSize;
	NSSize persistedSize = NSSizeFromString(persistedSizeString);
	NSSize maxSize = [element maximumSize];
	NSSize minSize = [element minimumSize];
	if (persistedSize.width > maxSize.width || persistedSize.height > maxSize.height || 
		persistedSize.width < minSize.width || persistedSize.height < minSize.height) {
		[[NSUserDefaults standardUserDefaults] removeObjectForKey:objectKey];
		return NSZeroSize;
	}
	return persistedSize;
}

-(void)updateVisibleElements_ {
	NSArray *visibleElements = [self visibleElements_];
	if ([[visibleElements valueForKey:@"view"] isEqualToArray:[self subviews]]) 
		return;
	NSMutableArray *visibleViews = [NSMutableArray arrayWithCapacity:[visibleElements count]];
	for (ALTimelineElement *element in visibleElements) {
		NSView *elementView = [element view];
		[elementView setFrame:[element currentPosition]];
		[visibleViews addObject:elementView];
	}
	[self setSubviews:visibleViews];
}


#pragma mark -
#pragma mark Geometry

-(ALTimelineElement *)elementAtPoint:(NSPoint)aPoint {
	for (ALCanvasPage *page in [self pages]) {
		if (![page pointInExtendedPage:aPoint])
			continue;
		ALTimelineElement *foundElement = [page elementAtPoint:aPoint];
		if (foundElement)
			return foundElement;
	}
	return nil;
}

-(NSArray *)elementsInRect:(NSRect)timelineFrame {
	BOOL countPages = NO;
	NSPoint topRight = NSMakePoint(NSMaxX(timelineFrame), NSMaxY(timelineFrame));
	NSMutableArray *candidateElements = [NSMutableArray array];
	for (ALCanvasPage *page in [self pages]) {
		if (!countPages && [page pointInExtendedPage:timelineFrame.origin])
			countPages = YES;
		if (!countPages)
			continue;
		[candidateElements addObjectsFromArray:[page elements]];
		if ([page pointInPage:topRight])
			break;
	}
	NSMutableArray *elementsInRect = [NSMutableArray array];
	for (ALTimelineElement *element in candidateElements) {
		if (NSIntersectsRect(timelineFrame, [element currentPosition]))
			[elementsInRect addObject:element];
	}
	return elementsInRect;
}

-(NSRect)useableRectForBounds:(NSRect)bounds {
	NSRect rect = NSInsetRect(bounds, ALTimelineCanvasEndPadding, 0);
	rect.origin.y += ALTimelineCanvasBottomPadding;
	rect.size.height -= ALTimelineCanvasBottomPadding + ALTimelineCanvasTopPadding;
	return rect;
}


#pragma mark -
#pragma mark Date/position conversions

-(CGFloat)positionOfDate:(NSDate *)aDate {
	NSArray *elements = [[self timeline] timelineElements];
	if ([elements count] == 0)
		return 0;
	ALTimelineElement *firstElement = [elements objectAtIndex:0];
	NSDate *firstDate = [firstElement date];
	NSUInteger deltaDays = [aDate smrDaysAfterDate:firstDate];
	CGFloat points = deltaDays * [self pointsPerDay];
	CGFloat firstX = [firstElement currentPosition].origin.x;
	return firstX + points;
}

-(NSDate *)dateAtPosition:(CGFloat)xPosition {
	NSArray *elements = [[self timeline] timelineElements];
	if ([elements count] == 0)
		return nil;
	ALTimelineElement *firstElement = [elements objectAtIndex:0];
	CGFloat firstX = [firstElement currentPosition].origin.x;
	CGFloat deltaPoints = xPosition - firstX;
	NSInteger deltaDays = (NSInteger) deltaPoints / [self pointsPerDay];
	NSDateComponents *daysToAdd = [[NSDateComponents alloc] init];
	[daysToAdd setDay:deltaDays];
	NSDate *firstDate = [firstElement date];
	NSCalendar *calendar = [NSCalendar currentCalendar];
	NSDate *result = [calendar dateByAddingComponents:daysToAdd toDate:firstDate options:0];
	return result;
}


#pragma mark -
#pragma mark Mouse event handling

-(BOOL)acceptsFirstResponder {
	return YES;
}

-(void)mouseDown:(NSEvent *)mouseDownEvent {
	BOOL trackingWhileInBackground = ![NSApp isActive];
	NSUInteger modifiers = [mouseDownEvent modifierFlags];
	BOOL usingCMD = (modifiers & NSCommandKeyMask) != 0;
	if (usingCMD)
		[NSApp preventWindowOrdering];
	NSPoint mouseDownPoint = [self convertPoint:[mouseDownEvent locationInWindow] fromView:nil];
	ALTimelineElement *mouseDownElement = [self elementAtPoint:mouseDownPoint];
	NSPoint elementMouseDownPoint = [[mouseDownElement view] convertPoint:[mouseDownEvent locationInWindow] fromView:nil];
	ALResizeDirection direction = [mouseDownElement mouseInResizeTrackingArea:elementMouseDownPoint];
	if (mouseDownElement && direction != ALResizeDirectionNone) {
		NSRect useableRect = [self useableRectForBounds:[self bounds]];
		[mouseDownElement trackResizeWithEvent:mouseDownEvent withinRect:useableRect];
		[self setFixedLayoutElement:mouseDownElement];
		[self setNeedsLayout:YES];
		return;
	}
	if (!mouseDownElement) 
		[[self timeline] deselectAllElements];
	NSTimer *autoScrollTimer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(autoScrollTimerDidFire_:) userInfo:nil repeats:YES];
	[[NSRunLoop currentRunLoop] addTimer:autoScrollTimer forMode:NSEventTrackingRunLoopMode];
	ALRubberBandView *rubberBand = [[ALRubberBandView alloc] initWithFrame:NSZeroRect];
	[self addSubview:rubberBand positioned:NSWindowAbove relativeTo:nil];
	NSEvent *currentEvent = [[self window] nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask];
	while ([currentEvent type] != NSLeftMouseUp) {
		NSPoint currentMousePoint = [self convertPoint:[currentEvent locationInWindow] fromView:nil];
		CGFloat movementDistance = hypot(mouseDownPoint.x - currentMousePoint.x, mouseDownPoint.y - currentMousePoint.y);
		if ([self dragDropDelegate] && mouseDownElement && movementDistance >= ALTimelineCanvasMinimumDragDistance) {
			[autoScrollTimer invalidate];
			[self initiateDragWithEvent_:currentEvent forElement:mouseDownElement];
			return;
		}
		else if (movementDistance >= ALTimelineCanvasMinimumDragDistance) {
			NSRect dragRect = [self rectFromPoint_:mouseDownPoint toPoint:currentMousePoint];
			[rubberBand setFrame:dragRect];
			NSArray *elementsInRect = [self elementsInRect:dragRect];
			[[self timeline] selectElements:elementsInRect extendSelection:NO];
		}
		currentEvent = [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:YES];
	}
	[autoScrollTimer invalidate];
	[rubberBand removeFromSuperview];
	if (trackingWhileInBackground && usingCMD) {
		[[self timeline] selectElement:mouseDownElement extendSelection:NO];
		return;
	}
	if (mouseDownElement && !trackingWhileInBackground) 
		[self handleClickWithEvent_:currentEvent];
}

-(void)handleClickWithEvent_:(NSEvent *)mouseUpEvent {
	NSPoint mouseUpPoint = [self convertPoint:[mouseUpEvent locationInWindow] fromView:nil];
	ALTimelineElement *selectedElement = [self elementAtPoint:mouseUpPoint];
	NSPoint elementPoint = [[selectedElement view] convertPoint:mouseUpPoint fromView:self];
	if (!selectedElement) {
		[[self timeline] deselectAllElements];
		return;
	}
	if ([mouseUpEvent clickCount] == 2) {
		[[self timeline] editElement:selectedElement atPoint:elementPoint];
		return;
	}
	[[self timeline] scrollElementToVisible:selectedElement];
	NSUInteger modifiers = [mouseUpEvent modifierFlags];
	BOOL extendSelection = modifiers & NSShiftKeyMask || modifiers & NSCommandKeyMask;
	if (extendSelection && [selectedElement selected]) 
		[[self timeline] deselectElement:selectedElement];
	else 
		[[self timeline] selectElement:selectedElement extendSelection:extendSelection];
}

-(void)autoScrollTimerDidFire_:(NSTimer *)timer {
	NSEvent *currentEvent = [NSApp currentEvent];
	if ([currentEvent type] == NSLeftMouseDragged)
		[self autoscroll:currentEvent];
}


#pragma mark -
#pragma mark Key event handling

-(void)keyDown:(NSEvent *)theEvent {
    if ([[self window] firstResponder] == self && ([[self delegate] respondsToSelector:@selector(timeline:doCommandBySelector:)] || [[self delegate] respondsToSelector:@selector(timeline:insertText:)])) {
		[self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
	}
	else
		[super keyDown:theEvent];
}

-(void)doCommandBySelector:(SEL)aSelector {
	LogDebug(@"%@", NSStringFromSelector(aSelector));
	if ([[self delegate] respondsToSelector:@selector(timeline:doCommandBySelector:)] && ![[self delegate] timeline:[self timeline] doCommandBySelector:aSelector])
		[super doCommandBySelector:aSelector];
}

-(void)insertText:(id)insertString {
	if ([[self delegate] respondsToSelector:@selector(timeline:insertText:)] && ![[self delegate] timeline:[self timeline] insertText:insertString])
		[super insertText:insertString];
}

-(void)insertTab:(id)sender {
	if ([[self window] firstResponder] == self)
		[[self window] selectNextKeyView:self];
}

-(void)insertBacktab:(id)sender {
	if ([[self window] firstResponder] == self)
		[[self window] selectPreviousKeyView:self];
}

-(void)moveLeft:(id)sender {
	NSEvent *currentEvent = [NSApp currentEvent];
	BOOL usingShift = ([currentEvent modifierFlags] & NSShiftKeyMask) != 0;
	[[self timeline] selectPreviousElementAndExtendSelection:usingShift];
}

-(void)moveRight:(id)sender {
	NSEvent *currentEvent = [NSApp currentEvent];
	BOOL usingShift = ([currentEvent modifierFlags] & NSShiftKeyMask) != 0;
	[[self timeline] selectNextElementAndExtendSelection:usingShift];
}

-(void)moveLeftAndModifySelection:(id)sender {
	[[self timeline] selectPreviousElementAndExtendSelection:YES];
}

-(void)moveRightAndModifySelection:(id)sender {
	[[self timeline] selectNextElementAndExtendSelection:YES];
}


#pragma mark -
#pragma mark Resize tracking areas

-(BOOL)isElementVisible:(ALTimelineElement *)element {
	return [[self visibleElements_] containsObject:element];
}


#pragma mark -
#pragma mark Drag & drop

-(void)initiateDragWithEvent_:(NSEvent *)theEvent forElement:(ALTimelineElement *)element {
	
}


#pragma mark -
#pragma mark Notifications

-(void)timelineElementsDidChange:(NSNotification *)note {
	NSDictionary *userInfo = [note userInfo];
	[self setAddedElements:[userInfo valueForKey:ALTimelineInsertedEntriesKey]];
	[self initializeAddedElements_];
	[self setRemovedElements:[userInfo valueForKey:ALTimelineRemovedEntriesKey]];
	[self setNeedsLayout:YES];
}

-(void)viewDidScroll:(NSNotification *)note {
	[self updateVisibleElements_];
}

-(void)canvasDidChangeFrame:(NSNotification *)note {
	[self setNeedsLayout:[self isLayoutRequiredForSize:[self bounds].size]];
}

-(void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
	ALTimelineElement *animatedElement = [theAnimation valueForKey:ALTimelineAnimatedElement];
	if (!animatedElement || animatedElement != [self fixedLayoutElement]) 
		return;
	[[self timeline] scrollElementToVisible:animatedElement];
	[self setFixedLayoutElement:nil];
}

#pragma mark -
#pragma mark Utilities

-(NSRect)rectFromPoint_:(NSPoint)firstPoint toPoint:(NSPoint)secondPoint {
	CGFloat smallestX = fmin(firstPoint.x, secondPoint.x);
	CGFloat largestX = fmax(firstPoint.x, secondPoint.x);
	CGFloat smallestY = fmin(firstPoint.y, secondPoint.y);
	CGFloat largestY = fmax(firstPoint.y, secondPoint.y);
	return NSMakeRect(smallestX, smallestY, largestX - smallestX, largestY - smallestY);
}

@end
